package net.cathackers.devel.scmp.chat.impl.xmpp;

import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import net.cathackers.devel.scmp.config.impl.SCMPConstants;
import net.cathackers.devel.scmp.factories.SCMPFactory;

import org.apache.log4j.Logger;
import org.apache.mina.core.buffer.IoBuffer;

public class XMLCumulativeParser {
    private static int maxBufferSize_;
    // Chars that represent CDATA section start
    private static char[] _CDATA_START_ = {'<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['};
    // Chars that represent CDATA section end
    private static char[] _CDATA_END_ = {']', ']', '>'};

    // Buffer with all data retrieved
    private StringBuilder buffer_ = new StringBuilder();
    
    private Logger logger_;
    
    // ---- INTERNAL STATUS -------
    // Initial status
    private static final int _INIT_ = 0;
    // Status used when the first tag name is retrieved
    private static final int _HEAD_ = 2;
    // Status used when robot is inside the XML and it looking for the tag conclusion
    private static final int _INSIDE_ = 3;
    // Status used when a '<' is found and try to find the conclusion tag.
    private static final int _PRETAIL_ = 4;
    // Status used when the ending tag is equal to the head tag
    private static final int _TAIL_ = 5;
    // Status used when robot is inside the main tag and found an '/' to check '/>'.
    private static final int _VERIFY_CLOSE_TAG_ = 6;
    //  Status used when you are inside a parameter
    private static final int _INSIDE_PARAM_VALUE_ = 7;
    //  Status used when you are inside a CDATA section
    private static final int _INSIDE_CDATA_ = 8;
    // Status used when you are outside a tag/reading text
    private static final int _OUTSIDE_ = 9;
    
    final String[] sstatus_ = {"INIT", "", "HEAD", "INSIDE", "PRETAIL", "TAIL", "VERIFY", "INSIDE_PARAM", "INSIDE_CDATA", "OUTSIDE"};

    // Current robot status
    private int status_ = XMLCumulativeParser._INIT_;

    // Index to looking for a CDATA section start or end.
    private int cdataOffset_ = 0;

    // Number of chars that matches with the head tag. If the tailCount is equal to
    // the head length so a close tag is found.
    private int tailCount_ = 0;
    // Indicate the starting point in the buffer for the next message.
    private int startLastMsg_ = 0;
    // Flag used to discover tag in the form <tag />.
    private boolean insideRootTag_ = false;
    // Object containing the head tag
    private StringBuilder head_ = new StringBuilder(5);
    // List with all finished messages found.
    private List<String> msgs_ = new ArrayList<String>();
    private int depth_ = 0;

    Charset encoder_;

    static {
        // Set default max buffer size to 1MB. If limit is reached then close connection
        maxBufferSize_ = 1048576;
    }

    public XMLCumulativeParser() {
        encoder_ = Charset.forName(SCMPConstants._ENCODING_);
        logger_  = SCMPFactory.getLogger(this);
    }

    /*
    * true if the parser has found some complete xml message.
    */
    public boolean doHaveMessages() {
        return (msgs_.size() > 0);
    }

    /*
    * @return an array with all messages found
    */
    public String[] getMessages() {
        String[] res = new String[msgs_.size()];
        for (int i = 0; i < res.length; i++) {
            res[i] = msgs_.get(i);
        }
        msgs_.clear();
        invalidateBuffer();
        return res;
    }

    /*
    * Method use to re-initialize the buffer
    */
    private void invalidateBuffer() {
        if (buffer_.length() > 0) {
            String str = buffer_.substring(startLastMsg_);
            buffer_.delete(0, buffer_.length());
            buffer_.append(str);
            buffer_.trimToSize();
        }
        startLastMsg_ = 0;
    }


    /*
    * Method that add a message to the list and reinit parser.
    */
    private void addToFoundMessages(String msg) {
        // Add message to the complete message list
        if (msg != null) {
            msgs_.add(msg);
        }
        // Move the position into the buffer
        status_ = XMLCumulativeParser._INIT_;
        tailCount_ = 0;
        cdataOffset_ = 0;
        head_.setLength(0);
        insideRootTag_ = false;
        depth_ = 0;
    }

    /*
    * Main reading method
    */
    public void read(IoBuffer byteBuffer) throws Exception {
        invalidateBuffer();
        // Check that the buffer is not bigger than 1 Megabyte. For security reasons
        // we will abort parsing when 1 Mega of queued chars was found.
        if (buffer_.length() > maxBufferSize_) {
            throw new Exception("Stopped parsing never ending stanza");
        }
        CharBuffer charBuffer = encoder_.decode(byteBuffer.buf());
        char[] buf = charBuffer.array();
        int readByte = charBuffer.remaining();

        // Just return if nothing was read
        if (readByte == 0) {
            return;
        }

        // Verify if the last received byte is an incomplete double byte character
        char lastChar = buf[readByte-1];
        if (lastChar >= 0xfff0) {
            logger_.debug("Waiting to get complete char: " + String.valueOf(buf));
            // Rewind the position one place so the last byte stays in the buffer
            // The missing byte should arrive in the next iteration. Once we have both
            // of bytes we will have the correct character
            byteBuffer.position(byteBuffer.position()-1);
            // Decrease the number of bytes read by one
            readByte--;
            // Just return if nothing was read
            if (readByte == 0) {
                return;
            }
        }

        buffer_.append(buf, 0, readByte);
        // Do nothing if the buffer only contains white spaces
        if (buffer_.charAt(0) <= ' ' && buffer_.charAt(buffer_.length()-1) <= ' ') {
            if ("".equals(buffer_.toString().trim())) {
                // Empty the buffer so there is no memory leak
                buffer_.delete(0, buffer_.length());
                return;
            }
        }
        // Robot.
        char ch;
        boolean isHighSurrogate = false;
        for (int i = 0; i < readByte; i++) {
            ch = buf[i];
            if (ch < 0x20 && ch != 0x9 && ch != 0xA && ch != 0xD && ch != 0x0) {
                 //Unicode characters in the range 0x0000-0x001F other than 9, A, and D are not allowed in XML
                 //We need to allow the NULL character, however, for Flash XMLSocket clients to work.
                throw new Exception("Disallowed character");
            }
            if (isHighSurrogate) {
                if (Character.isLowSurrogate(ch)) {
                    // Everything is fine. Clean up traces for surrogates
                    isHighSurrogate = false;
                }
                else {
                    // Trigger error. Found high surrogate not followed by low surrogate
                    throw new Exception("Found high surrogate not followed by low surrogate");
                }
            }
            else if (Character.isHighSurrogate(ch)) {
                isHighSurrogate = true;
            }
            else if (Character.isLowSurrogate(ch)) {
                // Trigger error. Found low surrogate char without a preceding high surrogate
                throw new Exception("Found low surrogate char without a preceding high surrogate");
            }
            if (status_ == XMLCumulativeParser._TAIL_) {
                // Looking for the close tag
                if (depth_ < 1 && ch == head_.charAt(tailCount_)) {
                    tailCount_++;
                    if (tailCount_ == head_.length()) {
                        // Close stanza found!
                        // Calculate the correct start,end position of the message into the buffer
                        int end = buffer_.length() - readByte + (i + 1);
                        String msg = buffer_.substring(startLastMsg_, end);
                        // Add message to the list
                        addToFoundMessages(msg);
                        startLastMsg_ = end;
                    }
                } else {
                    tailCount_ = 0;
                    status_ = XMLCumulativeParser._INSIDE_;
                }
            } else if (status_ == XMLCumulativeParser._PRETAIL_) {
                if (ch == XMLCumulativeParser._CDATA_START_[cdataOffset_]) {
                    cdataOffset_++;
                    if (cdataOffset_ == XMLCumulativeParser._CDATA_START_.length) {
                        status_ = XMLCumulativeParser._INSIDE_CDATA_;
                        cdataOffset_ = 0;
                        continue;
                    }
                } else {
                    cdataOffset_ = 0;
                    status_ = XMLCumulativeParser._INSIDE_;
                }
                if (ch == '/') {
                    status_ = XMLCumulativeParser._TAIL_;
                    depth_--;
                }
                else if (ch == '!') {
                    // This is a <! (comment) so ignore it
                    status_ = XMLCumulativeParser._INSIDE_;
                }
                else {
                    depth_++;
                }
            } else if (status_ == XMLCumulativeParser._VERIFY_CLOSE_TAG_) {
                if (ch == '>') {
                    depth_--;
                    status_ = XMLCumulativeParser._OUTSIDE_;
                    if (depth_ < 1) {
                        // Found a tag in the form <tag />
                        int end = buffer_.length() - readByte + (i + 1);
                        String msg = buffer_.substring(startLastMsg_, end);
                        // Add message to the list
                        addToFoundMessages(msg);
                        startLastMsg_ = end;
                    } 
                } else if (ch == '<') {
                    status_ = XMLCumulativeParser._PRETAIL_;
                } else {
                    status_ = XMLCumulativeParser._INSIDE_;
                }
            } else if (status_ == XMLCumulativeParser._INSIDE_PARAM_VALUE_) {

                if (ch == '"') {
                    status_ = XMLCumulativeParser._INSIDE_;
                }
            } else if (status_ == XMLCumulativeParser._INSIDE_CDATA_) {
                if (ch == XMLCumulativeParser._CDATA_END_[cdataOffset_]) {
                    cdataOffset_++;
                    if (cdataOffset_ == XMLCumulativeParser._CDATA_END_.length) {
                        status_ = XMLCumulativeParser._OUTSIDE_;
                        cdataOffset_ = 0;
                    }
                } else {
                    cdataOffset_ = 0;
                }
            } else if (status_ == XMLCumulativeParser._INSIDE_) {
                if (ch == XMLCumulativeParser._CDATA_START_[cdataOffset_]) {
                    cdataOffset_++;
                    if (cdataOffset_ == XMLCumulativeParser._CDATA_START_.length) {
                        status_ = XMLCumulativeParser._INSIDE_CDATA_;
                        cdataOffset_ = 0;
                        continue;
                    }
                } else {
                    cdataOffset_ = 0;
                    status_ = XMLCumulativeParser._INSIDE_;
                }
                if (ch == '"') {
                    status_ = XMLCumulativeParser._INSIDE_PARAM_VALUE_;
                } else if (ch == '>') {
                    status_ = XMLCumulativeParser._OUTSIDE_;
                    if (insideRootTag_ && ("stream:stream>".equals(head_.toString()) ||
                            ("?xml>".equals(head_.toString())) || ("flash:stream>".equals(head_.toString())))) {
                        // Found closing stream:stream
                        int end = buffer_.length() - readByte + (i + 1);
                        // Skip LF, CR and other "weird" characters that could appear
                        while (startLastMsg_ < end && '<' != buffer_.charAt(startLastMsg_)) {
                            startLastMsg_++;
                        }
                        String msg = buffer_.substring(startLastMsg_, end);
                        addToFoundMessages(msg);
                        startLastMsg_ = end;
                    }
                    insideRootTag_ = false;
                } else if (ch == '/') {
                    status_ = XMLCumulativeParser._VERIFY_CLOSE_TAG_;
                }
            } else if (status_ == XMLCumulativeParser._HEAD_) {
                if (ch == ' ' || ch == '>') {
                    // Append > to head to allow searching </tag>
                    head_.append(">");
                    if(ch == '>')
                        status_ = XMLCumulativeParser._OUTSIDE_;
                    else
                        status_ = XMLCumulativeParser._INSIDE_;
                    insideRootTag_ = true;
                    continue;
                }
                else if (ch == '/' && head_.length() > 0) {
                    status_ = XMLCumulativeParser._VERIFY_CLOSE_TAG_;
                    depth_--;
                }
                head_.append(ch);

            } else if (status_ == XMLCumulativeParser._INIT_) {
                if (ch == '<') {
                    status_ = XMLCumulativeParser._HEAD_;
                    depth_ = 1;
                }
                else {
                    startLastMsg_++;
                }
            } else if (status_ == XMLCumulativeParser._OUTSIDE_) {
                if (ch == '<') {
                    status_ = XMLCumulativeParser._PRETAIL_;
                    cdataOffset_ = 1;
                }
            }
        }
        if (head_.length() > 0 &&
                ("/stream:stream>".equals(head_.toString()) || ("/flash:stream>".equals(head_.toString())))) {
            // Found closing stream:stream
            addToFoundMessages("</stream:stream>");
        }
    }

}
